/* ver: 0.1, date 30-12-2007, by Marcel Hekman
 * Created the initial skeleton of the gui.
 * ver: 0.2, date 02-02-2007, by Marcel Hekman
 * - implemented ActionListener
 * - reflectStateChange()
 * - build and solve functions
 * - WorkerThreads
 * ver: 0.3, dat 02-01-2008, by Jeroen te Strake
 * - Updated build radiobutton handling to use individual
 *   listeners (prevents crash)
 * ver: 0.4, date 03-02-2007, by Marcel Hekman
 * - Cache the previous state of the manager to reduce the amount of GUI updates (for performance).
 * ver: 0.5, date 04-01-2007, by Jeroen S
 * Benchmarking 
 * ver: 0.6, date 04-02-2007, by Marcel Hekman
 * - Radiobutton refactored. 
 * - Cache the previous state of the manager to reduce the amount of GUI updates (for performance).
 * ver: 0.7, date 04-02-2007, by Marcel Hekman
 * - Post build gedeelte toegevoegd aan de GUI.
 * - Beetje geschoven met de layout.
 */

package mazeAssignment;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

public class MazeGui implements ActionListener
{
	//GUI constants.
	public static final int MIN_Y_SIZE = 600;
	public static final int MENU_PANEL_WIDTH = 260;
	public static final int STATUS_PANEL_HEIGHT = 70;
	public static final int SLIDER_MIN = 0;
	public static final int SLIDER_MAX = 100;
	public static final int INSET_SIZE = 1;
	

	public static enum MazeType {SingleArrayMaze, DoubleArrayMaze, CompactArrayMaze};
	
	public static enum BuildAlgorithm
	{
		BookBuild, PathBuild, DfsBuild,
		KruskalBuild, KruskalBuild2, PathBuildBinaryTree, DfsBuildRewrite, DfsBuildRewrite2
	};
	
	public static enum SolveAlgorithm
	{
		RightHandSolve, UnweightedShortestSolve, DijkstraShortestSolve, RightHandSolveStack,
		DeadendFillSolve, DeadendChaseSolve, 
	};
	
	
	private Manager manager;
	private MazeGraph mazePanel;
	private Maze maze;
	
	private Container mTopLevelContainer;	//JFrame or JApplet.
	private Container mContentPane;
	
	//General options panel
	private JSlider delaySlider;
	private JTextField txtMazeBreadth;
	private JTextField txtMazeHeight;
	private JButton btnUpdateSize;
	private JToggleButton tbtnBuildColors;
	
	//Build options panel
	private JComboBox<String> comboboxBuild;
	private JButton btnBuildAndPause;
	private JButton btnResetAll;
	
	//Post build options panel
	private JTextField txtRemoveWalls;
	private JCheckBox cboxRandomizeStartEnd;
	private JButton btnPostBuild;
	
	//Solve options panel
	private JComboBox<String> comboboxSolve;
	private JButton btnSolveAndPause;
	private JButton btnResetSolve;
	
	//Benchmark panel
	private JLabel lblBuildTotalLabel;
	private JLabel lblBuildTotal;
	private JLabel lblBuildTickCountLabel;
	private JLabel lblBuildTickCount;
	private JLabel lblBuildTimePerTickLabel;
	private JLabel lblBuildTimePerTick;
	private JLabel lblSolveTotalLabel;
	private JLabel lblSolveTotal;
	private JLabel lblSolveTickCountLabel;
	private JLabel lblSolveTickCount;
	private JLabel lblSolveTimePerTickLabel;
	private JLabel lblSolveTimePerTick;
	
	
	
	//Current GUI state
	private int currentGuiState;
	
	/**
	 * Constructs the entire GUI.
	 * @param pManager The manager class, used to manage Build and Solve states.
	 * @param pMaze The maze used in the maze panel.
	 * @param pTopLevelContainer The top level container either JFrame or JApplet.
	 * @param pContentPane The contentpane inside the pTopLevelContainer.
	 */
	public MazeGui(Manager pManager, Maze pMaze, Container pTopLevelContainer, Container pContentPane)
	{
		//Should be run from the event thread.
		if(! SwingUtilities.isEventDispatchThread())
			throw new WrongThreadException("MazeGui() constructor");
		
		mTopLevelContainer = pTopLevelContainer;
		mContentPane = pContentPane;
		
		this.manager = pManager;
		fillContentPane();
		insertMazePanel(pMaze);
		currentGuiState = -1;		//To force reflectStateChange() to update the gui.
		reflectStateChange();
		
		pTopLevelContainer.setVisible(true);
	}
	
	
	
	/**
	 * Called from the manager to display the an error message.
	 */
	public void displayErrorMessage(String message, String title)
	{
		//Should be run from the event thread.
		if(! SwingUtilities.isEventDispatchThread())
			throw new WrongThreadException("MazeGui.displayErrorMessage()");
		
		JOptionPane.showMessageDialog(mTopLevelContainer, message, title, JOptionPane.ERROR_MESSAGE);
	}
	
	/**
	 * Handles the button presses.
	 * This function validates button presses and text field inputs.
	 * Other logic is in the separate functions.
	 */
	@Override
	public void actionPerformed(ActionEvent event)
	{
		int state = manager.getState();
		
		if(event.getSource() == btnUpdateSize)
		{
			//Should only be handled when the manager is not busy.
			if(! manager.isBusy())
			{
				//Check if the user wants to loose any current maze data (if any)
				if(state == Manager.STATE_BUILD_PAUSED || state == Manager.STATE_SOLVE_PAUSED)
				{
					boolean abortResize = 
						JOptionPane.showConfirmDialog(
							mTopLevelContainer,
							"Resizing will cause the current maze to get removed, continue?",
							"Erase current maze?",
							JOptionPane.OK_CANCEL_OPTION,
							JOptionPane.QUESTION_MESSAGE) != JOptionPane.OK_OPTION;
					
					//Abort if the user selected cancel
					if(abortResize)
						return;
				}
				
				try
				{
					int xSize = Integer.parseInt( txtMazeBreadth.getText() );
					int ySize = Integer.parseInt( txtMazeHeight.getText() );
					
					//Should 1 or larger at the very least.
					if(xSize < 2 || ySize < 2)
						throw new NumberFormatException();
					
					handleResize(xSize, ySize);
				}
				catch(NumberFormatException e)
				{
					displayErrorMessage("Specify valid values in 'breadth' and 'height'.", "Invalid input");
					return;
				}
			}
		}
		else if(event.getSource() == btnBuildAndPause)
		{
			int delay = delaySlider.getValue();
			if(delay < SLIDER_MIN || SLIDER_MAX < delay)
			{
				displayErrorMessage("Slider value is outside of slider bounds, should not happend!!!", "Paranoia error");
				return;
			}
			
			//This button has 3 functions, start, pause and unpause.
			switch(state)
			{
			//Start
			case Manager.STATE_CLEAR:
			case Manager.STATE_BUILD_FINISHED:
			case Manager.STATE_SOLVE_PAUSED:
			case Manager.STATE_SOLVED:
			{
				startBuild(delay);
				break;
			}
			//Pause
			case Manager.STATE_BUILDING:
			{
				manager.pauseBuild();
				break;
			}
			//Resume
			case Manager.STATE_BUILD_PAUSED:
			{
				continueBuild(delay);
				break;
			}
			case Manager.STATE_SOLVING:
			default:
			{
				displayErrorMessage("Invalid state: "+state+" encountered in actionListener for btnBuildAndPause.", "Paranoia error");
				return;
			}
			}
		}
		else if(event.getSource() == btnResetAll)
		{
			switch(state)
			{
			case Manager.STATE_BUILDING:
			case Manager.STATE_SOLVING:
			{
				displayErrorMessage("An operation is in progress, cannot reset!", "Paranoia error");
				return;
			}
			case Manager.STATE_BUILD_PAUSED:
			case Manager.STATE_BUILD_FINISHED:
			case Manager.STATE_SOLVE_PAUSED:
			case Manager.STATE_SOLVED:
			case Manager.STATE_CLEAR:
			{
				manager.clearAll();
				
				return;
			}
			}
		}
		else if(event.getSource() == btnPostBuild)
		{
			//Validate the manager state.
			if( manager.getState() != Manager.STATE_BUILD_FINISHED )
			{
				displayErrorMessage("This function is only available after the build stage.", "Paranoia error");
				return;
			}
			
			int wallsToKnock;
			boolean doRandomExit = cboxRandomizeStartEnd.isSelected();
			
			//Validate the removewall text field.
			try
			{
				wallsToKnock = Integer.parseInt(txtRemoveWalls.getText());
				
				if(wallsToKnock < 0)
					throw new NumberFormatException();
			}
			catch(NumberFormatException ne)
			{
				displayErrorMessage("Remove walls has to be a number of 0 or higher.", "Input error");
				return;
			}
			
			//Call the function to process the request.
			manager.intermediaryStep(doRandomExit, wallsToKnock);
			
			//Update the mazePanel
			mazePanel.repaint();
		}
		else if(event.getSource() == btnSolveAndPause)
		{
			int delay = delaySlider.getValue();
			if(delay < SLIDER_MIN || SLIDER_MAX < delay)
			{
				displayErrorMessage("Slider value is outside of slider bounds, should not happend!!!", "Paranoia error");
				return;
			}
			
			//This button has 3 functions, start, pause and unpause.
			switch(state)
			{
			//Start
			case Manager.STATE_BUILD_FINISHED:
			case Manager.STATE_SOLVED:
			{
				startSolve(delay);
				break;
			}
			//Pause
			case Manager.STATE_SOLVING:
			{
				manager.pauseSolve();
				break;
			}
			//Resume
			case Manager.STATE_SOLVE_PAUSED:
			{
				continueSolve(delay);
				break;
			}
			case Manager.STATE_CLEAR:
			case Manager.STATE_BUILDING:
			case Manager.STATE_BUILD_PAUSED:
			default:
			{
				displayErrorMessage("Invalid state: "+state+" encountered in actionListener for btnSolveAndPause.", "Paranoia error");
				return;
			}
			}
		}
		else if(event.getSource() == btnResetSolve)
		{
			switch(state)
			{
			case Manager.STATE_BUILDING:
			case Manager.STATE_SOLVING:
			{
				displayErrorMessage("An operation is in progress, cannot reset!", "Paranoia error");
				return;
			}
			case Manager.STATE_BUILD_PAUSED:
			case Manager.STATE_BUILD_FINISHED:
			{
				displayErrorMessage("Nothing to reset, use 'Reset all'.", "Paranoia error");
				return;
			}
			case Manager.STATE_SOLVE_PAUSED:
			case Manager.STATE_SOLVED:
			case Manager.STATE_CLEAR:
			{
				manager.clearSolveState();
				return;
			}
			}
		}
		else if(event.getSource() == tbtnBuildColors)
		{
			this.mazePanel.setDrawClrSquares( tbtnBuildColors.isSelected() );
			this.mazePanel.repaint();
		}
	}
	
	
	/*
	 * ##### EVENT HANDLE FUNCTIONS #####
	 */
	
	/**
	 * Starts a worker thread to start a build operation.
	 * @throws ClassNotFoundException 
	 */
	private void startBuild(int delay)
	{
		int buildAlgorithm = comboboxBuild.getSelectedIndex();
		
		try
		{
			new Thread(new WTStartBuild(buildAlgorithm, delay), "BuildThread").start();
		}
		catch(Exception ex)
		{
			displayErrorMessage(ex.getMessage(), "Exception starting build algorithm");
		}
	}
	
	/**
	 * Starts a worker thread to continue the build operation.
	 */
	private void continueBuild(int delay)
	{
		//Start thread to continue the build.
		new Thread(new WTContinueBuild(delay), "BuildThread").start();
	}
	
	/**
	 * Starts a worker thread to start a solve operation.
	 */
	private void startSolve(int delay)
	{
		int solveAlgorithm = comboboxSolve.getSelectedIndex();
		
		//Start the solve thread.
		try
		{
			new Thread(new WTStartSolve(solveAlgorithm, delay), "SolveThread").start();
		}
		catch(Exception ex)
		{
			displayErrorMessage(ex.getMessage(), "Exception starting solve algorithm");
		}
	}

	/**
	 * Starts a worker thread to continue the solve operation.
	 */
	private void continueSolve(int delay)
	{
		new Thread(new WTContinueSolve(delay), "SolveThread").start();
	}
	
	/**
	 * Called by the actionListener to adjust the maze size.
	 * @param x Width
	 * @param y Height
	 */
	private void handleResize(int x, int y)
	{
		Maze maze = manager.resizeMaze(x, y);
		
		//Remove the old mazePanel
		mContentPane.remove( mazePanel );
		
		//Insert a new maze panel
		insertMazePanel(maze);
		
		//Reflect the state 
		this.reflectStateChange();
	}
	
	

	/**
	 * Is called to reflect changes in the state of the manager.
	 * Should only be called from the event dispatching thread.
	 */
	public void reflectStateChange()
	{
		//Should be run from the event thread.
		if(! SwingUtilities.isEventDispatchThread())
			throw new WrongThreadException("MazeGui.reflectStateChange()");
		
		int state = manager.getState();
		
		//Only reformat when needed.
		if(state == currentGuiState)
			return;
		
		currentGuiState = state;
		
		switch( state )
		{
		case Manager.STATE_CLEAR:
		{
			btnUpdateSize.setEnabled(true);
			comboboxBuild.setEnabled(true);
			btnBuildAndPause.setText("Build");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(false);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(true);
			btnSolveAndPause.setText("Solve");
			btnSolveAndPause.setEnabled(false);
			btnResetSolve.setEnabled(false);
			break;
		}
		case Manager.STATE_BUILDING:
		{
			btnUpdateSize.setEnabled(false);
			comboboxBuild.setEnabled(false);
			btnBuildAndPause.setText("Pause");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(false);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(true);
			btnSolveAndPause.setText("Solve");
			btnSolveAndPause.setEnabled(false);
			btnResetSolve.setEnabled(false);
			break;
		}
		case Manager.STATE_BUILD_PAUSED:
		{
			btnUpdateSize.setEnabled(true);
			comboboxBuild.setEnabled(false);
			btnBuildAndPause.setText("Resume");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(true);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(true);
			btnSolveAndPause.setText("Solve");
			btnSolveAndPause.setEnabled(false);
			btnResetSolve.setEnabled(false);
			break;
		}
		case Manager.STATE_BUILD_FINISHED:
		{
			btnUpdateSize.setEnabled(true);
			comboboxBuild.setEnabled(true);
			btnBuildAndPause.setText("Build");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(true);
			btnPostBuild.setEnabled(true);
			comboboxSolve.setEnabled(true);
			btnSolveAndPause.setText("Solve");
			btnSolveAndPause.setEnabled(true);
			btnResetSolve.setEnabled(false);
			break;
		}
		case Manager.STATE_SOLVING:
		{
			btnUpdateSize.setEnabled(false);
			comboboxBuild.setEnabled(true);
			btnBuildAndPause.setText("Build");
			btnBuildAndPause.setEnabled(false);
			btnResetAll.setEnabled(false);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(false);
			btnSolveAndPause.setText("Pause");
			btnSolveAndPause.setEnabled(true);
			btnResetSolve.setEnabled(false);
			break;
		}
		case Manager.STATE_SOLVE_PAUSED:
		{
			btnUpdateSize.setEnabled(true);
			comboboxBuild.setEnabled(true);
			btnBuildAndPause.setText("Build");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(true);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(false);
			btnSolveAndPause.setText("Resume");
			btnSolveAndPause.setEnabled(true);
			btnResetSolve.setEnabled(true);
			break;
		}
		case Manager.STATE_SOLVED:
		{
			btnUpdateSize.setEnabled(true);
			comboboxBuild.setEnabled(true);
			btnBuildAndPause.setText("Build");
			btnBuildAndPause.setEnabled(true);
			btnResetAll.setEnabled(true);
			btnPostBuild.setEnabled(false);
			comboboxSolve.setEnabled(true);
			btnSolveAndPause.setText("Solve");
			btnSolveAndPause.setEnabled(true);
			btnResetSolve.setEnabled(true);
			break;
		}
		
		default:
		{
			throw new RuntimeException("Unknown state encountered in MazeGui.reflectStateChange().");
		}
		}

		//Reformat the components
		btnUpdateSize.invalidate();
		btnBuildAndPause.invalidate();
		btnResetAll.invalidate();
		btnSolveAndPause.invalidate();
		btnResetSolve.invalidate();
	}
	
	/**
	 * Used to update relevant benchmarks in its panel 
	 * @param buildTime Total time to build
	 * @param buildTick Ticks done to build
	 * @param solveTime Total time to solve
	 * @param solveTick Ticks done to solve
	 */
	public void updateBenchmark(long buildTime, long buildTick,
								long solveTime, long solveTick)
	{
		//Should be run from the event thread.
		if(! SwingUtilities.isEventDispatchThread())
			throw new WrongThreadException("MazeGui.updateBenchmark()");
		
		lblBuildTotal.setText(Long.toString(buildTime));
		lblBuildTickCount.setText(Long.toString(buildTick));
		if(buildTick > 0)
			lblBuildTimePerTick.setText(Long.toString(buildTime/buildTick));
		lblSolveTotal.setText(Long.toString(solveTime));
		lblSolveTickCount.setText(Long.toString(solveTick));
		if(solveTick > 0)
			lblSolveTimePerTick.setText(Long.toString(solveTime/solveTick));		
	}
	
	/*
	 * ###### INITIALISATION FUNCTIONS ######
	 */
	
	/**
	 * Used to adjust the GUI to a maze object.
	 * Does not remove the old maze.
	 * @param maze
	 */
	private void insertMazePanel(Maze maze)
	{
		mTopLevelContainer.setVisible(false);
		
		this.maze = maze;
		
		double xSizeMaze = maze.getSizeX();
		double ySizeMaze = maze.getSizeY();
		
		//Default size is the minimum size of the menu panel.
		double xSizeMazePanel = Integer.MAX_VALUE;
		double ySizeMazePanel = Integer.MAX_VALUE;
		
		ArrayList<Double> factors = new ArrayList<Double>();
		for(double i = 200; i >= 0; i--) {
			factors.add(i);
		}
		factors.add(0.5);
		factors.add(0.25);
		factors.add(0.125);
		
		//Enforce a max width
		Dimension screenSize = mTopLevelContainer.getToolkit().getScreenSize();
		int maxWidth = (screenSize.width - MENU_PANEL_WIDTH) - 100;
		int maxHeight = (screenSize.height - STATUS_PANEL_HEIGHT) - 100;
		
		for(int i = 0; i < factors.size() && (xSizeMazePanel > maxWidth || ySizeMazePanel > maxHeight); i++) {
			xSizeMazePanel = xSizeMaze * factors.get(i);
			ySizeMazePanel = ySizeMaze * factors.get(i);
		}
		xSizeMazePanel = Math.max(xSizeMazePanel, MIN_Y_SIZE);
		ySizeMazePanel = Math.max(ySizeMazePanel, MIN_Y_SIZE);
		
		//If we're in a JFrame set the size of the frame, no point in setting JApplet size.
		if(mTopLevelContainer.getClass() == JFrame.class)
		{
			int xSize = MENU_PANEL_WIDTH + (int)xSizeMazePanel + 17;
			int ySize = (int)ySizeMazePanel + STATUS_PANEL_HEIGHT + 24;
			mTopLevelContainer.setSize(xSize, ySize);
		}
		
		mazePanel = new MazeGraphNew(maze);
		mazePanel.setDrawClrSquares( this.tbtnBuildColors.isSelected() );
		mazePanel.setPreferredSize(new Dimension((int)xSizeMazePanel, (int)ySizeMazePanel));
		
		mContentPane.add( mazePanel, BorderLayout.CENTER );
		
		//frame.pack()
		mazePanel.invalidate();
		mContentPane.validate();
		
		//Re-center JFrame after maze resize.
		if(mTopLevelContainer.getClass() == JFrame.class)
		{
			((JFrame)mTopLevelContainer).setLocationRelativeTo(null);
		}
		mTopLevelContainer.setVisible(true);
	}
	
	/**
	 * Fills the ContentPane of JFrame or JApplet.
	 */
	private void fillContentPane()
	{
		mContentPane.setLayout(new BorderLayout());
		mContentPane.add( makeMenuPanel(), BorderLayout.WEST );
		mContentPane.add( makeBenchmarkPanel(), BorderLayout.SOUTH);
	}
	
	/**
	 * Generates the menu JPanel.
	 * Called from constructor -> makeFrame().
	 */
	private JPanel makeMenuPanel()
	{
		JPanel menuPanel = new JPanel();
		
		menuPanel.setLayout(new BoxLayout(menuPanel, BoxLayout.PAGE_AXIS));
		menuPanel.setPreferredSize(new Dimension(MENU_PANEL_WIDTH, 0));
		menuPanel.add( makeGeneralOptionsPanel() );
		menuPanel.add( makeBuildOptionsPanel() );
		menuPanel.add( makePostBuildOptionsPanel() );
		menuPanel.add( makeSolveOptionsPanel() );

		//Enable Debug maze import/exporing
		enableDebugImportExport(menuPanel);
		
		//Benchmark panel, not available from an JApplet.
		if(mTopLevelContainer.getClass() == JFrame.class)
		{
			enableBenchmarkButton(menuPanel);
		}
		
		return menuPanel;
	}	
	
	private JPanel makeGeneralOptionsPanel()
	{
		JPanel generalOptionsPanel = new JPanel();
		generalOptionsPanel.setBorder(BorderFactory.createTitledBorder("General Options"));
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		generalOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		
		//Speed slider label
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 0;
		generalOptionsPanel.add(new JLabel("Algorithm delay (in ms):"), c);
		c.gridy = 1;
		delaySlider = new JSlider(SLIDER_MIN, SLIDER_MAX, Manager.DEFAULT_DELAY);
		delaySlider.setMajorTickSpacing(20);
		delaySlider.setMinorTickSpacing(5);
		delaySlider.setPaintLabels(true); 
		delaySlider.setPaintTicks(true);
		generalOptionsPanel.add(delaySlider, c);
		c.gridwidth = 1;
		
		//Maze breath
		c.gridx = 0;
		c.gridy = 2;
		generalOptionsPanel.add(new JLabel("Maze breadth:"), c);
		c.gridx = 1;
		txtMazeBreadth = new JTextField( Integer.toString(Manager.DEFAULT_MAZE_SIZE_X) );
		txtMazeBreadth.setColumns(8);
		txtMazeBreadth.setHorizontalAlignment(JTextField.RIGHT);
		generalOptionsPanel.add(txtMazeBreadth, c);
		
		//Maze height
		c.gridx = 0;
		c.gridy = 3;
		generalOptionsPanel.add(new JLabel("Maze height:"), c);
		c.gridx = 1;
		txtMazeHeight = new JTextField( Integer.toString(Manager.DEFAULT_MAZE_SIZE_Y) );
		txtMazeHeight.setColumns(8);
		txtMazeHeight.setHorizontalAlignment(JTextField.RIGHT);
		generalOptionsPanel.add(txtMazeHeight, c);
		
		//Update size
		c.gridx = 0;
		c.gridy = 4;
		btnUpdateSize = new JButton("Update size");
		btnUpdateSize.addActionListener(this);
		generalOptionsPanel.add(btnUpdateSize, c);
		
		//Build colors toggle button
		c.gridx = 1;
		tbtnBuildColors = new JToggleButton("Build colors");
		tbtnBuildColors.setSelected(false);
		tbtnBuildColors.addActionListener(this);
		generalOptionsPanel.add(tbtnBuildColors, c);
		
		return generalOptionsPanel;
	}	
	
	private JPanel makeBuildOptionsPanel()
	{
		JPanel buildOptionsPanel = new JPanel();
		buildOptionsPanel.setBorder(BorderFactory.createTitledBorder("Build Options"));
		
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		buildOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		//Generate the build algorithm combobox
		comboboxBuild = new JComboBox<String>();
		for(int i = 0; i < BuildAlgorithm.values().length; i++)
		{
			comboboxBuild.addItem( BuildAlgorithm.values()[i].toString() );
		}
		
		//Add the build algorithm combobox to the layout
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 0;
		buildOptionsPanel.add(new JLabel("Algorithm:"), c);
		c.gridx = 0;
		c.gridy++;
		buildOptionsPanel.add(comboboxBuild, c);
		
		c.gridwidth = 1;
		
		
		//Build / Stop button
		c.gridx = 0;
		c.gridy++;
		btnBuildAndPause = new JButton("Build");
		btnBuildAndPause.addActionListener(this);
		buildOptionsPanel.add(btnBuildAndPause, c);
		
		//Reset button
		c.gridx = 1;
		btnResetAll = new JButton("Reset all");
		btnResetAll.addActionListener(this);
		buildOptionsPanel.add(btnResetAll, c);
		
		
		return buildOptionsPanel;
	}
	
	private JPanel makePostBuildOptionsPanel()
	{
		JPanel postBuildOptionsPanel = new JPanel();
		postBuildOptionsPanel.setBorder(BorderFactory.createTitledBorder("Post Build Options"));
		
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		postBuildOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);

		
		//Walls to remove
		c.gridx = 0;
		c.gridy = 0;
		postBuildOptionsPanel.add(new JLabel("Walls to try to remove:"), c);
		c.gridx = 1;
		txtRemoveWalls = new JTextField("0");
		txtRemoveWalls.setColumns(8);
		txtRemoveWalls.setHorizontalAlignment(JTextField.RIGHT);
		postBuildOptionsPanel.add(txtRemoveWalls, c);
		
		//Generate the checkbox
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 1;
		cboxRandomizeStartEnd = new JCheckBox("Randomize start and end");
		postBuildOptionsPanel.add(cboxRandomizeStartEnd, c);
		c.gridwidth = 1;
		
		//Process button
		c.gridx = 0;
		c.gridy = 2;
		btnPostBuild = new JButton("Process");
		btnPostBuild.setEnabled(false);
		btnPostBuild.addActionListener(this);
		postBuildOptionsPanel.add(btnPostBuild, c);
		
		return postBuildOptionsPanel;
	}
	
	private JPanel makeSolveOptionsPanel()
	{
		JPanel solveOptionsPanel = new JPanel();
		solveOptionsPanel.setBorder(BorderFactory.createTitledBorder("Solve Options"));
		
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		solveOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		//Generate the solve algorithm combobox.
		comboboxSolve = new JComboBox<String>();
		for(int i = 0; i < SolveAlgorithm.values().length; i++)
		{
			comboboxSolve.addItem( SolveAlgorithm.values()[i].toString() );
		}
		
		//Add the solve algorithm combobox to the panel.
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 0;
		solveOptionsPanel.add(new JLabel("Algorithm:"), c);
		c.gridx = 0;
		c.gridy++;
		solveOptionsPanel.add(comboboxSolve, c);
		
		c.gridwidth = 1;
		
		//Solve / Stop button
		c.gridx = 0;
		c.gridy++;
		btnSolveAndPause = new JButton("Solve");
		btnSolveAndPause.setEnabled(false);
		btnSolveAndPause.addActionListener(this);
		solveOptionsPanel.add(btnSolveAndPause, c);
		
		//Reset button
		c.gridx = 1;
		btnResetSolve = new JButton("Reset solve");
		btnResetSolve.setEnabled(false);
		btnResetSolve.addActionListener(this);
		solveOptionsPanel.add(btnResetSolve, c);
		
		return solveOptionsPanel;
	}
	
	private JPanel makeBenchmarkPanel()
	{
		JPanel benchmarkPanel = new JPanel();
		benchmarkPanel.setBorder(BorderFactory.createTitledBorder("Benchmarking"));
		
		GridLayout grid = new GridLayout(2, 6);
		benchmarkPanel.setLayout(grid);
		
		lblBuildTotalLabel = new JLabel("Total Build Time: ");
		benchmarkPanel.add(lblBuildTotalLabel);
		lblBuildTotal = new JLabel("0");
		benchmarkPanel.add(lblBuildTotal);
		lblBuildTickCountLabel = new JLabel("Build Tick Count: ");
		benchmarkPanel.add(lblBuildTickCountLabel);
		lblBuildTickCount = new JLabel("0");
		benchmarkPanel.add(lblBuildTickCount);
		lblBuildTimePerTickLabel = new JLabel("Build Time Per Tick: ");
		benchmarkPanel.add(lblBuildTimePerTickLabel);
		lblBuildTimePerTick = new JLabel("0");
		benchmarkPanel.add(lblBuildTimePerTick);
		lblSolveTotalLabel = new JLabel("Total Solve Time: ");
		benchmarkPanel.add(lblSolveTotalLabel);
		lblSolveTotal = new JLabel("0");
		benchmarkPanel.add(lblSolveTotal);
		lblSolveTickCountLabel = new JLabel("Solve Tick Count: ");
		benchmarkPanel.add(lblSolveTickCountLabel);
		lblSolveTickCount = new JLabel("0");
		benchmarkPanel.add(lblSolveTickCount);
		lblSolveTimePerTickLabel = new JLabel("Solve Time Per Tick: ");
		benchmarkPanel.add(lblSolveTimePerTickLabel);
		lblSolveTimePerTick = new JLabel("0");
		benchmarkPanel.add(lblSolveTimePerTick);

		return benchmarkPanel;
	}
	
	/*
	 * Function for Manager to repaint the maze display after each tick
	 */
	public void repaintMazeGraph()
	{
		mazePanel.repaint();
	}
	
	/*
	 * Private classes used to start worker threads.
	 * (WT stands for worker thread)
	 */
	private class WTStartBuild implements Runnable
	{
		private Build algorithm;
		private int delay;
		
		public WTStartBuild(int a, int d) throws ClassNotFoundException, InstantiationException, IllegalAccessException
		{
			//Using reflection to retrieve the build algorithm class.
			//Retrieve the current build algorithm.
			MazeGui.BuildAlgorithm ba = MazeGui.BuildAlgorithm.values()[a];
			
			//Retrieve the current build class by using the java classloader.
			Class<?> buildClass = Class.forName(
					this.getClass().getPackage().getName()+".build."+ba.toString() );
			
			//Execute the default constructor of the Build object.
			this.algorithm = (Build)buildClass.newInstance();
			
			this.delay = d;
		}
		
		@Override
		public void run()
		{
			manager.startBuild(algorithm, delay);
		}
	}
	
	private class WTContinueBuild implements Runnable
	{
		private int delay;
		
		public WTContinueBuild(int d)
		{
			this.delay = d;
		}
		
		@Override
		public void run()
		{
			manager.continueBuild(this.delay);
		}
	}
	
	private class WTStartSolve implements Runnable
	{
		private Solve algorithm;
		private int delay;
		
		public WTStartSolve(int a, int d) throws ClassNotFoundException, InstantiationException, IllegalAccessException
		{
			//Using reflection to retrieve the build algorithm class.
			//Retrieve the current build algorithm.
			MazeGui.SolveAlgorithm sa = MazeGui.SolveAlgorithm.values()[a];
			
			//Retrieve the current build class by using the java classloader.
			Class<?> solveClass = Class.forName(
					this.getClass().getPackage().getName()+".solve."+sa.toString() );
			
			//Execute the default constructor of the Build object.
			this.algorithm = (Solve)solveClass.newInstance();
			
			this.delay = d;
		}
		
		@Override
		public void run()
		{
			manager.startSolve(algorithm, delay);
		}
	}
	
	private class WTContinueSolve implements Runnable
	{
		private int delay;
		
		public WTContinueSolve(int d)
		{
			this.delay = d;
		}
		
		@Override
		public void run()
		{
			manager.continueSolve( this.delay );
		}
	}

	/**
	 * Used to enable the MazeImporter functionality used for debugging.
	 */
	public void enableDebugImportExport(JPanel pMenuPanel)
	{
		//Open Action object
		Action openAction = new AbstractAction(){
			private static final long serialVersionUID = 1L;

			public void actionPerformed(ActionEvent e)
			{
				if(manager.isBusy())
					return;
					
				//Load the maze
				Maze maze = MazeImporter.loadMaze(mTopLevelContainer);
				
				if(maze != null)
				{
					//Update the manager
					manager.setMaze(maze);
	
					//Remove the old mazePanel
					mContentPane.remove( mazePanel );
					
					//Insert a new mazePanel
					MazeGui.this.insertMazePanel(maze);
				}
			}
		};
		//Open shortcut (Ctrl + O)
		KeyStroke openStroke = KeyStroke.getKeyStroke("ctrl O");
		pMenuPanel.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW).put(openStroke, "openAction");
		pMenuPanel.getActionMap().put("openAction", openAction);
		
		
		//Save Action object
		Action saveAction = new AbstractAction(){
			private static final long serialVersionUID = 1L;
			
			public void actionPerformed(ActionEvent e) {
				if(manager.isBusy())
					return;
				
				MazeImporter.saveMaze(mTopLevelContainer, maze);
			}
		};
		//Save shortcut (Ctrl + S)
		KeyStroke saveStroke = KeyStroke.getKeyStroke("ctrl S");
		pMenuPanel.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW).put(saveStroke, "saveAction");
		pMenuPanel.getActionMap().put("saveAction", saveAction);
	}
	
	public void enableBenchmarkButton(JPanel pMenuPanel)
	{
		//Benchmark Action object
		Action benchmarkAction = new AbstractAction(){
			private static final long serialVersionUID = 1L;
	
			public void actionPerformed(ActionEvent e)
			{
				if(manager.isBusy())
					return;
				
				((JFrame)mTopLevelContainer).dispose();
				
				new Benchmark();
			}
		};
		//Benchmark shortcut (Ctrl + B)
		KeyStroke benchmarkStroke = KeyStroke.getKeyStroke("ctrl B");
		pMenuPanel.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW).put(benchmarkStroke, "benchmarkAction");
		pMenuPanel.getActionMap().put("benchmarkAction", benchmarkAction);
	}
}
